﻿using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using Logger;
using SocketCore.Decoder;
using SocketCore.Encoder;
using SocketCore.Event;
using SocketCore.Exception;

namespace SocketCore.Network
{
    /// <summary>
    /// Listening and sending data to clients
    /// </summary>
    public class SocketService : ISocketService
    {
        #region private member

        private IMessageDecoder decoder = MessageDecoderFactory.Instance.GetMessageDecoder(NetworkConfig.DecoderType);
        private IMessageEncoder encoder = MessageEncoderFactory.Instance.GetMessageEncoder(NetworkConfig.EncoderType);
        private ReaderWriterLock endpointsLock = new ReaderWriterLock();
        private NetworkHandlerChain networkHandlerChain;
        private readonly Dictionary<EndPoint, Socket> servers = new Dictionary<EndPoint, Socket>(5);
        private const int WAIT_TIME = 30000;

        #endregion

        #region methods

        public void Bind(EndPoint endPoint, INetworkHandler handler)
        {
            this.networkHandlerChain = new NetworkHandlerChain(handler);
            this.InitListener(endPoint);
        }

        public void Bind(EndPoint endPoint, NetworkHandlerChain networkHandlerChain)
        {
            this.networkHandlerChain = networkHandlerChain;
            this.InitListener(endPoint);
        }

        public bool CheckDos(IoSession session)
        {
            try
            {
                long totalCount = session.GetMemProperty<long>("TotalCount") + 1L;
                session.AddProperty("TotalCount", totalCount, true);
                long firstTime = session.GetMemProperty<long>("FirstTime");
                long passTime = (DateTime.Now.Ticks - firstTime) / 10000000;
                if (passTime != 0L)
                {
                    long avgMsgCount = totalCount / passTime;
                    if (avgMsgCount > 30)
                    {
                        Log.WriteLog(string.Concat(new object[] { "Flush ", avgMsgCount, " messages from: ", session.RemoteHost }));
                        session.Close();
                        return true;
                    }
                }
                return false;
            }
            catch (System.Exception ex)
            {
                Console.WriteLine(ex.ToString());
                return true;
            }
        }

        public void DisConnect(IoSession session)
        {
            DisconnectEvent disEvent = new DisconnectEvent(session);
            ThreadPool.QueueUserWorkItem(new WaitCallback(this.OnClosed), disEvent);
        }

        public byte[] GetDatagram(Message message, IoSession session)
        {
            byte[] data = this.encoder.Encode(message, session);
            int len = data.Length;
            byte[] datagram = new byte[NetworkConfig.DatagramHeaderLength + len];
            for (int i = NetworkConfig.DatagramHeaderLength - 1; i > -1; i--)
            {
                datagram[i] = (byte) len;
                len = len >> 8;
            }
            Array.Copy(data, 0, datagram, NetworkConfig.DatagramHeaderLength, data.Length);
            return datagram;
        }

        private void InitListener(EndPoint endPoint)
        {
            endpointsLock.AcquireWriterLock(WAIT_TIME);
            try
            {
                if (servers.ContainsKey(endPoint))
                {
                    throw new ArgumentException("This endpoint already in use.");
                }
                Socket server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                server.Bind(endPoint);
                server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
                server.NoDelay = true;
                server.Listen(NetworkConfig.PendingConnection);
                servers.Add(endPoint, server);
                int listenThreadCount = Environment.ProcessorCount / NetworkConfig.EndPointCount;
                Log.WriteLog(string.Concat(new object[] { "Start ", listenThreadCount, " thread(s) to accept connections on ", endPoint }));
                for (int i = 0; i < listenThreadCount; i++)
                {
                    server.BeginAccept(new AsyncCallback(this.OnConnectRequest), server);
                }
            }
            finally
            {
                endpointsLock.ReleaseWriterLock();
            }
        }

        public void NotifyError(IoSession session, System.Exception exception)
        {
            ExceptionEvent sessionEx = new ExceptionEvent(session, exception);
            ThreadPool.QueueUserWorkItem(new WaitCallback(this.OnError), sessionEx);
        }

        private void OnClosed(object state)
        {
            DisconnectEvent disConnectEvent = (DisconnectEvent) state;
            IoSession session = disConnectEvent.Session;
            try
            {
                if (session.Socket != null)
                {
                    try
                    {
                        session.Socket.Close();
                    }
                    catch
                    {
                    }
                    networkHandlerChain.FirstHandler().OnDisconnected(session);
                    session.Socket = null;
                }
            }
            catch
            {
            }
        }

        private void OnConnected(object state)
        {
            ConnectEvent conEvent = (ConnectEvent) state;
            IoSession session = conEvent.Session;
            try
            {
                this.networkHandlerChain.FirstHandler().OnConnected(session);
                session.ReadMessage();
            }
            catch (System.Exception e)
            {
                Log.WriteLog("Urgent !!! Error when notify new connection: " + e);
            }
        }

        private void OnConnectRequest(IAsyncResult ar)
        {
            Socket server = null;
            Socket client = null;
            try
            {
                server = (Socket) ar.AsyncState;
                client = server.EndAccept(ar);
            }
            catch (System.Exception e)
            {
                Log.WriteLog("Urgent !!! Error while accepting new connection: " + e);
            }
            try
            {
                server.BeginAccept(new AsyncCallback(this.OnConnectRequest), server);
            }
            catch (System.Exception e)
            {
                Log.WriteLog("Urgent !!! Cannot accept more connection: " + e);
                Log.WriteLog("Trying again...");
                Thread.Sleep(5000);
                
                if (server != null)
                {
                    try
                    {
                        server.BeginAccept(new AsyncCallback(this.OnConnectRequest), server);
                    }
                    catch (System.Exception)
                    {
                        Log.WriteLog("Urgent !!! Cannot accept more connection. Trying fail: " + e);
                    }
                }
                else
                {
                    Log.WriteLog("Server socket null.");
                }
            }
            if (client != null)
            {
                client.NoDelay = true;
                IoSession session = new IoSession(client, this);
                ConnectEvent conEvent = new ConnectEvent(session);
                session.AddProperty("FirstTime", DateTime.Now.Ticks, true);
                session.AddProperty("TotalCount", 0L, true);
                session.AddProperty("LastAccess", DateTime.Now.Ticks, true);
                ThreadPool.QueueUserWorkItem(new WaitCallback(this.OnConnected), conEvent);
            }
        }

        private void OnError(object state)
        {
            ExceptionEvent sessionEx = (ExceptionEvent) state;
            IoSession session = sessionEx.Session;
            try
            {
                this.networkHandlerChain.FirstHandler().OnError(sessionEx.Session, sessionEx.Ex);
            }
            catch
            {
            }
        }

        public void OnMessageReceived(object state)
        {
            MessageEvent msgEvent = (MessageEvent) state;
            try
            {
                IoSession session = msgEvent.Session;
                Message msg = msgEvent.Message;
                session.AddProperty("LastAccess", DateTime.Now.Ticks, true);

                if (msg.Type != PackageTypeEnum.Heartbeat)
                {
                    networkHandlerChain.FirstHandler().OnMessageReceived(session, msg);
                }
            }
            catch
            {
            }
        }

        private void OnMessageSent(object state)
        {
        }

        private void OnSent(IAsyncResult ar)
        {
            List<object> context = (List<object>) ar.AsyncState;
            IoSession session = (IoSession) context[0];
            Message message = (Message)context[1];
            Socket client = session.Socket;
            try
            {
                if (client.Connected)
                {
                    client.EndSend(ar);
                    MessageEvent msgEvent = new MessageEvent(session, message);
                    ThreadPool.QueueUserWorkItem(new WaitCallback(OnMessageSent), msgEvent);
                }
            }
            catch (System.Exception ex)
            {
                NotifyError(session, new SendException(session));
                Console.WriteLine(ex.ToString());
                Log.WriteLog(ex.ToString());
            }
        }

        private void OnSentSerialize(IAsyncResult ar)
        {
            List<object> infor = (List<object>) ar.AsyncState;
            IoSession session = (IoSession) infor[0];
            try
            {
                if (session.Socket.Connected)
                {
                    session.Socket.EndSend(ar);
                }
            }
            catch (System.Exception e)
            {
                NotifyError(session, new SendException(session));
                Console.WriteLine(e);
                Log.WriteLog(e.ToString());
            }
        }

        public void Send(IoSession session, byte[] message)
        {
            if (session.IsConnected)
            {
                List<object> objects = new List<object>(2);
                objects.Add(session);
                objects.Add(message);
                try
                {
                    if (session.Socket.Connected)
                    {
                        session.Socket.BeginSend(message, 0, message.Length, SocketFlags.None, 
                            new AsyncCallback(this.OnSentSerialize), objects);
                    }
                }
                catch (System.Exception ex)
                {
                    this.NotifyError(session, new SendException(session));
                    Console.WriteLine(ex.ToString());
                    Log.WriteLog(ex.ToString());
                }
            }
        }

        public void Send(IEnumerable<IoSession> sessions, Message message)
        {
            byte[] datagram = null;
            byte[] zipDatagram = null;
            try
            {
                foreach (IoSession session in sessions)
                {
                    try
                    {
                        if (!session.GetProperty<bool>("GZIP"))
                        {
                            if (datagram == null)
                            {
                                datagram = this.GetDatagram(message, session);
                            }
                            SendTo(message, datagram, session);
                        }
                        else
                        {
                            if (zipDatagram == null)
                            {
                                zipDatagram = this.GetDatagram(message, session);
                            }
                            SendTo(message, zipDatagram, session);
                        }
                        continue;
                    }
                    catch
                    {
                        continue;
                    }
                }
            }
            catch
            {
            }
        }

        public void Send(IoSession session, Message message)
        {
            byte[] datagram = this.GetDatagram(message, session);
            this.SendTo(message, datagram, session);
        }

        private void SendTo(Message message, byte[] datagram, IoSession session)
        {
            if (session.IsConnected)
            {
                List<object> objects = new List<object>(2);
                objects.Add(session);
                objects.Add(message);
                try
                {
                    if (session.Socket.Connected)
                    {
                        session.Socket.BeginSend(datagram, 0, datagram.Length, 
                            SocketFlags.None, new AsyncCallback(this.OnSent), objects);
                    }
                }
                catch (System.Exception ex)
                {
                    this.NotifyError(session, new SendException(session));
                    Console.WriteLine(ex.ToString());
                    Log.WriteLog(ex.ToString());
                }
            }
        }

        public void UnBind(EndPoint endPoint)
        {
            this.endpointsLock.AcquireWriterLock(WAIT_TIME);
            try
            {
                if (this.servers.ContainsKey(endPoint))
                {
                    this.servers[endPoint].Close();
                    this.servers.Remove(endPoint);
                }
            }
            finally
            {
                this.endpointsLock.ReleaseWriterLock();
            }
        }

        public void UnBindAll()
        {
            this.endpointsLock.AcquireWriterLock(WAIT_TIME);
            try
            {
                foreach (Socket sock in servers.Values)
                {
                    sock.Close();
                }
            }
            finally
            {
                endpointsLock.ReleaseWriterLock();
            }
            servers.Clear();
        }

        #endregion

        #region properties

        public IMessageDecoder Decoder
        {
            get
            {
                return this.decoder;
            }
            set
            {
                this.decoder = value;
            }
        }

        public IMessageEncoder Encoder
        {
            get
            {
                return this.encoder;
            }
            set
            {
                this.encoder = value;
            }
        }

        public NetworkHandlerChain NetworkHandlerChain
        {
            get
            {
                return this.networkHandlerChain;
            }
            set
            {
                this.networkHandlerChain = value;
            }
        }

        #endregion
    }
}